package com.googlecode.phelps;

import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.sql.DataSource;

public class ConnectionPool implements DataSource {
	
	private static final Logger log = Logger.getLogger(ConnectionPool.class.getName());

	// maximum connections this pool can open at any given time
	private int maxActiveConnections;
	
	// max time a thread can keep a connection checked out
	// this is to guard against leaks.  default: 10 minutes
	private long checkoutTimeoutMillis = 600000;
	
	// max time a connection can stay idle before it's closed
	// and removed from the pool.  default: 5 minutes
	private long idleTimeoutMillis = 300000;
	
	// time a thread is put to sleep before retries when
	// the pool is full
	private long retryIntervalMillis = 50;
	
	// frequency the pool is reaped for stale or idle
	// connections.  default: 30 seconds
	private long reapIntervalMillis = 30000;
	
	// max time a thread will wait for a connection before timing
	// out when the pool is full.  default: 15 minutes
	private long checkoutBlockingTimeoutMillis = 15 * 60000l;
	
	// next time that we should reap the pool
	private long nextTimeToReap = 0;
	
	// list of open connections that are not in use
	private List<ConnectionWrapper> idleConnectionList;
	
	// list of open, checked out (active) connections
	private List<ConnectionWrapper> checkedOutConnectionList;
	
	// number of times a connection has been checked out from the pool
	private long checkoutCountTotal = 0;
	
	// total time that connections have been checked out of the pool
	private long checkoutTimeTotal = 0;
	
	// jdbc params - used when we create a new Connection
	private String jdbcUsername;
	private String jdbcPassword;
	private String jdbcUrl;
	private String jdbcDriverClass;
	
	public ConnectionPool(int maxActiveConnections) {
		if (!(maxActiveConnections > 0))
			throw new IllegalArgumentException("maxActiveConnections must be > 0");
		
		this.maxActiveConnections = maxActiveConnections;
		this.idleConnectionList = new ArrayList<ConnectionWrapper>(maxActiveConnections);
		this.checkedOutConnectionList = new ArrayList<ConnectionWrapper>(maxActiveConnections);
	}
	
	public long getCheckoutCountTotal() {
		return checkoutCountTotal;
	}
	
	public long getCheckoutTimeTotal() {
		return checkoutTimeTotal;
	}
	
	public double getCheckoutTimeAverage() {
		if (checkoutCountTotal > 0)
			return checkoutTimeTotal / (1.0 * checkoutCountTotal);
		else
			return 0;
	}
	
	public synchronized int getCurrentCheckoutCount() {
		return checkedOutConnectionList.size();
	}
	
	
	public synchronized int getCurrentIdleCount() {
		return idleConnectionList.size();
	}
	
	public synchronized int getAvailableConnections() throws SQLException {
		reapStaleAndIdleConnections();
		return maxActiveConnections - getActiveConnections();
	}
	
	public synchronized int getActiveConnections() throws SQLException {
		reapStaleAndIdleConnections();
		return idleConnectionList.size() + checkedOutConnectionList.size();
	}
	
	public long getCheckoutBlockingTimeoutMillis() {
		return checkoutBlockingTimeoutMillis;
	}

	public void setCheckoutBlockingTimeoutMillis(long checkoutBlockingTimeoutMillis) {
		this.checkoutBlockingTimeoutMillis = checkoutBlockingTimeoutMillis;
	}

	public long getCheckoutTimeoutMillis() {
		return checkoutTimeoutMillis;
	}

	public void setCheckoutTimeoutMillis(long checkoutTimeoutMillis) {
		this.checkoutTimeoutMillis = checkoutTimeoutMillis;
	}

	public long getIdleTimeoutMillis() {
		return idleTimeoutMillis;
	}

	public void setIdleTimeoutMillis(long idleTimeoutMillis) {
		this.idleTimeoutMillis = idleTimeoutMillis;
	}

	public long getRetryIntervalMillis() {
		return retryIntervalMillis;
	}

	public void setRetryIntervalMillis(long retryIntervalMillis) {
		this.retryIntervalMillis = retryIntervalMillis;
	}

	public long getReapIntervalMillis() {
		return reapIntervalMillis;
	}

	public void setReapIntervalMillis(long reapIntervalMillis) {
		this.reapIntervalMillis = reapIntervalMillis;
	}

	public String getJdbcUsername() {
		return jdbcUsername;
	}
	
	public void setJdbcUsername(String jdbcUsername) {
		this.jdbcUsername = jdbcUsername;
	}

	public String getJdbcPassword() {
		return jdbcPassword;
	}

	public void setJdbcPassword(String jdbcPassword) {
		this.jdbcPassword = jdbcPassword;
	}

	public String getJdbcUrl() {
		return jdbcUrl;
	}

	public void setJdbcUrl(String jdbcUrl) {
		this.jdbcUrl = jdbcUrl;
	}

	public String getJdbcDriverClass() {
		return jdbcDriverClass;
	}

	public void setJdbcDriverClass(String jdbcDriverClass) throws ClassNotFoundException {
		Class.forName(jdbcDriverClass);
		this.jdbcDriverClass = jdbcDriverClass;
	}

	//////////////////////////////////////////////////////////////

	@Override
	public Connection getConnection() throws SQLException {
		
		long timeout = System.currentTimeMillis() + checkoutBlockingTimeoutMillis;
		
		while (System.currentTimeMillis() < timeout) {
			
			synchronized (this) {
				
				// reap stale connections
				reapStaleAndIdleConnections();
				
				// try to get an idle, already open connection
				ConnectionWrapper conn = getIdleConnection();
				
				// if no idle connection to return, try to 
				// create a new one
				if (conn == null)
					conn = createNewConnection();
				
				if (conn != null) {
					// we got a connection.   mark it checked out
					// and return it to the caller
					setCheckedOut(conn);
					return conn;
				}
			}
			
			// if we got here, the pool is full
			// sleep for a while and retry
			try {
				Thread.sleep(retryIntervalMillis);
			}
			catch (InterruptedException e) {
				break;
			}
		}
		
		throw new IllegalStateException("getConnection() loop aborted");
	}
	
	private synchronized ConnectionWrapper getIdleConnection() {
		if (idleConnectionList.size() > 0)
			return idleConnectionList.remove(0);
		else
			return null;
	}
	
	private synchronized ConnectionWrapper createNewConnection() throws SQLException {
		if (getActiveConnections() < maxActiveConnections) {
			Connection conn = DriverManager.getConnection(jdbcUrl, jdbcUsername, jdbcPassword);
			return new ConnectionWrapper(conn, this);
		}
		else {
			return null;
		}
	}
	
	private synchronized void setCheckedOut(ConnectionWrapper conn) {
		conn.setLastCheckoutTime(System.currentTimeMillis());
		checkedOutConnectionList.add(conn);
		checkoutCountTotal++;
	}
	
	protected synchronized void returnConnection(ConnectionWrapper conn) throws SQLException {
		checkoutTimeTotal += (System.currentTimeMillis() - conn.getLastCheckoutTime());
		checkedOutConnectionList.remove(conn);
		idleConnectionList.add(conn);
	}

	private synchronized void reapStaleAndIdleConnections() throws SQLException {
		long now = System.currentTimeMillis();
		if (now > nextTimeToReap) {
			
			this.checkedOutConnectionList = reapConnectionList(checkedOutConnectionList, checkoutTimeoutMillis);
			this.idleConnectionList = reapConnectionList(idleConnectionList, idleTimeoutMillis);
			
			nextTimeToReap = now + reapIntervalMillis;
		}
	}
	
	private List<ConnectionWrapper> reapConnectionList(List<ConnectionWrapper> list, long timeout) {
		long now = System.currentTimeMillis();
		
		ArrayList<ConnectionWrapper> newlist = new ArrayList<ConnectionWrapper>(maxActiveConnections);
		
		for (ConnectionWrapper conn : list) {
			long timeSinceCheckout = now - conn.getLastCheckoutTime();
			if (timeSinceCheckout > timeout) {
				log.info("Removing idle or stale connection.  timeSinceCheckout: " + timeSinceCheckout + " > " + timeout);
				killConnection(conn);
			}
			else {
				// keep connection
				newlist.add(conn);
			}
		}
		
		return newlist;
	}
	
	private synchronized void killConnection(ConnectionWrapper conn) {
		try {
			conn.closeConnection();
		}
		catch (SQLException e) {
			log.log(Level.SEVERE, "Error closing Connection", e);
		}
	}
	
	////////////////////////////////////////////////////////////////////////
	// Other DataSource methods
	// Need to examine whether we need to support any of these
	////////////////////////////////////////////////////////////////////////
	
	@Override
	public Connection getConnection(String username, String password)
			throws SQLException {
		
		throw new IllegalArgumentException("Unsupported.  Pelase use the no-arg getConnection() method");
	}

	@Override
	public PrintWriter getLogWriter() throws SQLException {
		return null;
	}

	@Override
	public int getLoginTimeout() throws SQLException {
		return 0;
	}

	@Override
	public void setLogWriter(PrintWriter out) throws SQLException {
	}

	@Override
	public void setLoginTimeout(int seconds) throws SQLException {
	}

	@Override
	public boolean isWrapperFor(Class<?> iface) throws SQLException {
		return false;
	}

	@Override
	public <T> T unwrap(Class<T> iface) throws SQLException {
		return null;
	}
	
}
